#!/usr/bin/env python
""" 
This module borrows heavily from python_daemon_ available at Pypi


.. _python_daemon: http://pypi.python.org/pypi/python-daemon/

@author: Jean-Lou Dupont
"""
__all__ = ['DaemonRunner',]

import errno
import signal
import os
import sys



# Imports from package ``python_daemon`` available on Pypi
# --------------------------------------------------------
try:
    from python_daemon.daemon      import DaemonContext
    from python_daemon.pidlockfile import PIDLockFile
except:
    # For the Windows based development environment...
    #  This helps me with the automatic Sphinx doc generation
    computer_name = str(os.environ['computername'])
    if (computer_name != "JLDUPONT"):
        print "python_daemon_tools requires `python_daemon` package available through Pypi"
        exit(0)    


# Local imports
import python_daemon_tools.helper



class DaemonRunnerException(Exception):
    """
    DaemonRunner Exception base class
    
    Allows for easier customization of error messages.
    Use `duck typing` to inspect the Exception for the 
    :attr:`msg_id` attribute.
    
    """
    def __init__(self, message, msg_id=None, params=None):
        Exception.__init__(self, message)
        self.msg_id = msg_id
        self.params = params


    

class DaemonRunner(object):
    """
    Controller class for a callable running in a separate background process
    
    The principal methods are:
    
    * :meth:`cmd_start`
    * :meth:`cmd_stop`
    
    
    The ``PID lock file`` is derived from ``app.name`` in the following way ::
    
        ${app.pid_directory}/${app.name}.pid
    
    and by default is located in the ``/var/run`` directory; this directory
    can be customized through the ``app.pid_directory`` attribute.
    
    The exceptions generated by this class contain `pseudo-messages` which are
    really meant as `index` to human readable messages. This way, customization
    is easier to handle.
    
    """
    
    messages_exceptions = []
    messages_logger     = []
    
    # defaults
    # ========
    _default_pid_filepath_prefix = "/var/run"
    
    
    def __init__(self, app, messages = None, logger = None):
        """
        The parameter ``app`` must be a callable with, as minimum, the following attributes:
        
        * *name*
        * *run()* method 
        
        Optional attributes are:
        
        * *before_start()* method
        * *before_run()* method

        The parameter *messages* must behaved like a dictionary. The list of messages for
        which the dictionary should have an entry is available through the class constant 
        :const:`messages_logger` for the logging activities and the class constant 
        :const:`messages_exception` for the exceptions.
        
        The parameter *logger* is meant to receive a compatible callable to the
        ``logging`` module. This parameter defaults to ``timed rotating`` file of the form ::
        
            /var/log/${app.name}.log

        Note that this logger is only active when the daemon is started: prior to this,
        only exceptions are raised for conferring error conditions.
        
        """
        #attributes
        self.app = app
        self.logger = logger
        self.messages = messages
        
        self.context = DaemonContext()
        
        #validations
        #  Don't init anything BEFORE going through
        #  this checkpoint
        self.validateApp()
        
        
        #initialization
        self._configSTDs()
        self._configPIDFile()



    def cmd_start(self):
        """
        Starts the daemon for ``app``
        
        The method ``app.before_start()`` is called prior to actually daemonizing;
        the method can abort the process by raising ...
        
        The method ``app.before_start()`` need not to exist (a validity check is performed).
        """
        if self.pidfile.is_locked():
            pidfile_path = self.pidfile.path
            
            if PIDFileHelper.pidfile_lock_is_stale(self.pidfile):
                self.pidfile.break_lock()
            else:
                ##EXCEPTION##
                self._raise('error_pidfile_locked', {'path':self.pidfile.path})
             
        
        # BEFORE START
        # ============
        abort = self._tryBeforeStart()
        if abort:
            self._raise('', {})   

        # START!!!
        # ========
        try:
            self.daemon_context.open()
        except Exception,e:
            ##EXCEPTION##
            self._raise('error_daemon_open', {'exc':e})

        # From this point on, use our configured logger
        # ---------------------------------------------
        self._configLogger()
        

        pid = os.getpid()
        self.logger.info('logger_daemon_start')
        
        # Before starting...
        abort = self._tryBeforeRun()
        if abort:
            ##EXCEPTION##
            self._raise('error_daemon_aborted', {})

        # RUN!!!
        # ======
        self.app.run()

        
    def cmd_stop(self):
        """
        Stops the daemon for (the currently running) ``app`` but not before calling ``app.stop``
        """
        if not self.pidfile.is_locked():
            pidfile_path = self.pidfile.path
            
            ##EXCEPTION##
            self._raise('error_pidfile_not_locked', {'path':pidfile_path})


        if PIDFileHelper.pidfile_lock_is_stale(self.pidfile):
            self.pidfile.break_lock()
        else:
            pid = self.pidfile.read_pid()
            try:
                os.kill(pid, signal.SIGTERM)
            except OSError, exc:
                ##EXCEPTION##
                self._raise('error_terminate_process', {'pid':pid})                
        
    def cmd_restart(self):
        """
        Restarts the daemon for (the currently running) ``app``
        """
        self.stop()
        self.start()



    # =========================================================
    # PRIVATE
    # =========================================================
    
    
    
    def _raise(self, msg, params=None):
        """
        Exception handling helper
        """
        raise DaemonRunnerException(msg, params)
    
    
    def _tryBeforeStart(self):
        """
        Gives a chance to the application to abort 
        prior to the ``start`` phase
        """
        return self.__tryAppMethod('before_start', '')

    def _tryBeforeRun(self):
        """
        Gives a chance to the application to abort 
        prior to the ``run`` phase        
        """
        return self.__tryAppMethod('before_run', '')
        
    def __tryAppMethod(self, method, msg):
        """
        """
        method = _secureGetFromApp(method, None)
        
        # Don't abort for nothing
        if method is None:
            return False
        
        if not callable(method):
            self._raise(msg, {})
            
        abort = method()
        return abort
    
    
    def _validateApp(self):
        """
        Performs some quick checks on the ``app`` attribute
        """
        required_attributes = ['name', 'run']
        
        # of course `app` must be configured
        if self.app is None:
            self._raise('', {})
        
        # and must be callable
        if not callable(self.app):
            self._raise('', {})

        def _checkAppAttr(app, attr):
            if not hasattr(app, attr):
                self._raise(msg, {})

        # check required parameters
        for attr in ReferenceApp.requiredAttributes():
            checkAppAttr(attr)
            
    
            

    def _configSTDs(self):
        """
        Configure stdin, stdout, stderr
        """
        stds = {    'stdin':    ('r',  {}), 
                    'stdout':   ('w+', {}),
                    'stderr':   ('w+', {'buffering':0} ) }
        
        # e.g. stdin_path  in `app`
        #  paths = { std: path }
        paths = dict( (var,getattr(self.app, std+'_path', None))
                    for std in stds )
        
        # skips the `None` ones
        handles = self._openStds( paths, stds )

        # configure the DaemonContext
        for var, handle in handles:
            setattr(self.context, var, handle)


    def _openStds(self, paths, stds):
        """
        Opens the files according to the filepaths, returns handles
        """
        # filter out `None` paths
        names = filter(lambda X: paths[X] is not None, paths)
        
        handles = {}
        
        for name in names:
            path   = paths[name]
            access = stds[name](0)
            params = stds[name](1) 
            handle = self._tryOpenStd(name, path, access, params)
            handles[name] = handle
        
        # { name:handle }    
        return handles
        
        
    def _tryOpenStd(self, name, path, access, params):
        """
        Guarded Open File
        """
        try:    handle = open(path, access, **params)
        except: self._raise('error_openfilepath', {'path':path, 'extra':name})
            
        return handle
        

    def _configPIDFile(self):
        """
        Configures the PID File
        """
        prefix = self._secureGetFromApp('pidfile_pathprefix', 
                                        self._default_pid_filepath_prefix)
        
        pidfile_path = prefix.rstrip('/') + "/" + self.app.name
        
        self.pidfile = PIDFileHelper.make_pidlockfile(pidfile_path)
        self.context.pidfile = self.pidfile       


    def _configLogger(self):
        """
        Configures the logger
        
        Verifies if a logger was specified at initialization time otherwise
        build a default one. 
        """
        if self.logger is None:
            self.logger = logger.getDefault( self.app.name )
         
            
    def _secureGetFromApp(self, param, default = None):
        """
        Gets a parameter from the ``app``
        """
        return getattr(self.app, param, default)




# ===============================================================================




class ReferenceApp(object):
    """
    ReferenceApp: used as model of an ``app``
    """
    
    _required_attributes = ['name', 'run']
    
    def requiredAttributes(self):
        """
        Generates the list of required attributes
        """
        for attr in self.required_attributes:
            yield attr
            
